package com.atguigu.gulimallcart.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.constant.CartConstant;
import com.atguigu.common.utils.R;
import com.atguigu.gulimallcart.config.CartThreadPoolConfig;
import com.atguigu.gulimallcart.feign.ProductFeignService;
import com.atguigu.gulimallcart.intercept.CartIntercept;
import com.atguigu.gulimallcart.service.CartService;
import com.atguigu.gulimallcart.vo.Cart;
import com.atguigu.gulimallcart.vo.CartItemVo;
import com.atguigu.gulimallcart.vo.SkuInfoVo;
import com.atguigu.gulimallcart.vo.UserInfoTo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;

@Service
public class CartServiceImpl implements CartService {

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    ProductFeignService productFeignService;

    @Autowired
    ThreadPoolExecutor executor;

    /**
     * 加入购物车
     */
    @Override
    public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        /**
         * 此方法是将商品数据存入redis，这里选用redis是因为添加购物车操作会频繁的交互数据库，但是redis的持久性有没有数据库好，所有需要增强redis的持久性
         *
         * 购物车存储结构： Map<（redis的key）用户id, Map<商品skuId, 商品信息>>
         * 1.判断用户是否登陆，分别存入redis（boundHashOps）
         * 2.调用商品服务，获取sku商品信息
         * 3.调用商品服务，获取sku的属性列表  SELECT CONCAT(a.`attr_name`,':',a.`attr_value`) FROM `pms_sku_sale_attr_value` a wher a.`sku_id` = ?
         * 4.加了线程池后，需要手动释放 threadlocal
         *
         * 为什么线程池不能放在common中？
         * 为什么使用同一个线程池？
         * 如果不添加自定义线程池会怎么样？ 如果不指定线程池，默认使用 ForkJoinPool，是守护线程，主线程结束，线程池关闭，自定义线程池的需要手动关闭，
         * 为什么通过调用get方法来完成操作，CompletableFuture.anyOf(skuRunAsync,skuAttrRunAsync).get(); ？ 等待所有线程执行结束
         * 为什么自定义线程后，调用线程却是调用 ThreadPoolExecutor ？
         * user-key是什么时候生成的？
         *
         */

        //绑定key值，不用要一直set key值，使用hash存储方便查询数据

        CartItemVo cartItemVo = new CartItemVo();
        BoundHashOperations<String, Object, Object> boundHashOperations = extracted();


        /**
         * 1.如果redis中已经保存了该商品，则只需要添加件数
         */
        String res = (String) boundHashOperations.get(Long.toString(skuId));
        if (!StringUtils.isEmpty(res)){
            CartItemVo itemVo = JSON.parseObject(res, CartItemVo.class);
            itemVo.setCount(itemVo.getCount() + num);
            String jsonString = JSON.toJSONString(itemVo);
            //这里是直接覆盖？
            boundHashOperations.put(Long.toString(skuId),jsonString);
            return itemVo;
        }

        /**
         * 2.如果redis中未保存该商品
         */
        //查询sku商品信息
        CompletableFuture<Void> skuRunAsync = CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                R<SkuInfoVo> info = productFeignService.info(skuId);
                if (info.getCode() == 0) {
                    SkuInfoVo skuInfoVo = info.getData(new TypeReference<SkuInfoVo>() {
                    });
                    cartItemVo.setSkuId(skuId);
                    cartItemVo.setCheck(true);
                    cartItemVo.setTitle(skuInfoVo.getSkuTitle());
                    cartItemVo.setImage(skuInfoVo.getSkuDefaultImg());
                    cartItemVo.setPrice(skuInfoVo.getPrice());
                    cartItemVo.setCount(num);
                }
            }
        }, executor);

        //查询sku属性列表
        CompletableFuture<Void> skuAttrRunAsync = CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                R<List<String>> skuAttr = productFeignService.getSkuAttr(skuId);
                if (skuAttr.getCode() == 0) {
                    List<String> skuAttrData = skuAttr.getData(new TypeReference<List<String>>() {
                    });
                    cartItemVo.setSkuAttrValues(skuAttrData);
                }
            }
        }, executor);

        //等待所有的异步任务全部完成
        CompletableFuture.allOf(skuRunAsync,skuAttrRunAsync).get();

        /**
         * redis存数据会使用序列号操作转成字符流进行保存，我们查询数据和获取都不方便，这里直接转换为JSON格式进行存储
         */
        //存入redis
        String cartJson = JSON.toJSONString(cartItemVo);
        boundHashOperations.put(Long.toString(skuId),cartJson);

        System.out.println();
        return cartItemVo;
    }

    /**
     * 根据skuId查询CartItemVo
     */
    @Override
    public CartItemVo getCartItem(Long skuId) {
        BoundHashOperations<String, Object, Object> extracted = extracted();
        String s = (String) extracted.get(skuId.toString());
        return JSON.parseObject(s, CartItemVo.class);
    }

    /**
     * 合并购物车
     *
     * 已登录
     *      1.获取已登录购物车数据
     *      2.获取离线购物车数据
     *          如果没有离线购物数据，则不操作
     *          如果有离线购物数据，有就取出数据
     *              将离线购物数据与已登录购物数据合并 -> 直接将离线数据添加到已登录的redis中，然后删除离线的redis
     * 离线
     *      1.获取离线购物车数据，直接展示
     */
    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        Cart cart = new Cart();
        UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
        //在线购物车（已登录用户的redis的key）
        String onlineKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
        //离线购物车（临时用户的redis的key）
        String offlineKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
        /**
         * 已登录
         */
        if (userInfoTo.getUserId() != null){

            /**
             * 这里是要做两次判断的
             * （1）如果离线购物车没数据，则只需要返回在线购物车的数据
             * （2）如果离线购物车有数据就合并数据
             *  上面两种判断都涉及到离线购物车的数据，所以应该先做离线操作，
             *  并且我们只需要返回在线购物车的数据，所以获取在线购物车的操作更因为放在最后执行
             *  总结：以后再碰见 if(xx == null){}else(xx != null){} 的操作应该直接走 if(xx != null){}
             */
            //判断离线是否有购物数据
            List<CartItemVo> offlineVos = getCartFromRedis(offlineKey);
            if (offlineVos != null){
                //如果离线购物车有数据，就合并数据->将离线数据添加到已登录的数据中
                for (CartItemVo offlineVo : offlineVos) {
                    /**
                     * 因为上面已经判断了用户已经登录，证明UserInfo的userId是存在的
                     * addToCart()也会通过extracted()得到已登录的redisKey
                     * 所有这里执行添加购物车的操作会直接添加到已登录的购物车中
                     */
                    addToCart(offlineVo.getSkuId(),offlineVo.getCount());
                    //删除离线数据
                    redisTemplate.delete(offlineKey);
                }
            }
            //再次查询在线购物车的数据，并返回
            cart.setItems(getCartFromRedis(onlineKey));
        }else {
            /**
             * 离线
             */
            cart.setItems(getCartFromRedis(offlineKey));
        }
        return cart;
    }


    /**
     * 根据key获取redis中存的值
     */
    @Override
    public List<CartItemVo> getCartFromRedis(String redisKey) {
        BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(redisKey);
        List<Object> values = ops.values();
        if (values != null && values.size() > 0){
            List<CartItemVo> collect = values.stream().map(obj -> {
                //因为我们存购物数据是使用的JSON格式，所有取数据时直接json字符串转java对象
                String str = (String) obj;
                CartItemVo itemVo = JSON.parseObject(str, CartItemVo.class);
                return itemVo;
            }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }


    @Override
    public void checkItem(Long skuId, Integer check) {
        BoundHashOperations<String, Object, Object> cartOps = extracted();
        //查询购物车里面的商品
        CartItemVo cartItem = getCartItem(skuId);
        //修改商品状态
        cartItem.setCheck(check == 1?true:false);
        //序列化存入redis中
        String redisValue = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),redisValue);
    }

    /**
     * 改变商品数量
     */
    @Override
    public void changeItemCount(Long skuId, Integer num) {
        //查询购物车里面的商品
        CartItemVo cartItem = getCartItem(skuId);
        cartItem.setCount(num);

        BoundHashOperations<String, Object, Object> cartOps = extracted();
        //序列化存入redis中
        String redisValue = JSON.toJSONString(cartItem);
        cartOps.put(skuId.toString(),redisValue);

    }

    /**
     * 删除商品
     */
    @Override
    public void deleteIdCartInfo(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = extracted();
        cartOps.delete(skuId.toString());
    }

    /**
     * 获取结算页面购物车数据
     */
    @Override
    public List<CartItemVo> orderFeignCart(Long id) {
        String redisKey = CartConstant.CART_PREFIX + id;
        List<CartItemVo> cartFromRedis = getCartFromRedis(redisKey);
        if (cartFromRedis != null){
            List<CartItemVo> collect = cartFromRedis.stream()
                    //筛选已勾选的商品
                    .filter(item -> item.getCheck())
                    .map(item -> {
                        //获取最新价格
                        BigDecimal price = productFeignService.getPrice(item.getSkuId());
                        item.setPrice(price);
                        return item;
                    }).collect(Collectors.toList());
            return collect;
        }
        return null;
    }


    /**
     * 封装redis的prefix前缀
     */
    private BoundHashOperations<String, Object, Object> extracted() {
        UserInfoTo userInfoTo = CartIntercept.toThreadLocal.get();
        String redisKey = "";
        //判断用户是否登陆->封装前缀
        if (userInfoTo.getUserId() != null){
            //gulimall:cart:1
            redisKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
        }else {
            redisKey = CartConstant.CART_PREFIX + userInfoTo.getUserKey();
        }
        //绑定key值
        BoundHashOperations<String, Object, Object> boundHashOperations = redisTemplate.boundHashOps(redisKey);
        return boundHashOperations;
    }
}
